EntityJoinTree.java
package org.codefilarete.stalactite.engine.runtime.load;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.TreeInflationContext;
import org.codefilarete.stalactite.engine.runtime.load.MergeJoinNode.MergeJoinRowConsumer;
import org.codefilarete.stalactite.mapping.AbstractTransformer;
import org.codefilarete.stalactite.mapping.EntityMapping;
import org.codefilarete.stalactite.mapping.RowTransformer;
import org.codefilarete.stalactite.query.model.Fromable;
import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.query.model.Query;
import org.codefilarete.stalactite.query.model.QueryStatement.PseudoColumn;
import org.codefilarete.stalactite.query.model.QueryStatement.PseudoTable;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.Union;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.bean.Randomizer;
import org.codefilarete.tool.bean.Randomizer.LinearRandomGenerator;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Collections;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.collection.Maps;
import org.codefilarete.tool.collection.ReadOnlyList;
import static org.codefilarete.stalactite.sql.ddl.structure.Table.COMPARATOR_ON_SCHEMA_AND_NAME;
/**
* Tree representing joins of a from clause, nodes are {@link JoinNode}.
* It maintains an index of its joins based on an unique name for each, so they can be referenced outside {@link EntityJoinTree} without
* depending on classes of this package (since the reference is a {@link String}).
*
* @author Guillaume Mary
*/
public class EntityJoinTree<C, I> {
/**
* Key of the very first {@link EntityJoinTree} added to the join structure (the one generated by constructor), see {@link #getRoot()}
*/
public static final String ROOT_JOIN_NAME = "ROOT";
private final JoinRoot<C, I, ?> root;
/**
* A mapping between a name and a join to find them when we want to join one with another new one
*
* @see #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)
* @see #indexKeyGenerator
*/
// Implemented as a LinkedHashMap to keep order only for debugging purpose
private final BidiMap<String, JoinNode<?, ?>> joinIndex = new DualLinkedHashBidiMap<>();
/**
* The objet that will help to give node names / keys into the index (no impact on the generated SQL)
*
* @see #joinIndex
*/
private final NodeKeyGenerator indexKeyGenerator = new NodeKeyGenerator();
// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
private final Set<Table<?>> tablesToExcludeFromDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
private final Set<Table<?>> tablesToIncludeToDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
public EntityJoinTree(EntityMapping<C, I, ?> entityMapping) {
this(new EntityInflater.EntityMappingAdapter<>(entityMapping), entityMapping.getTargetTable());
}
public EntityJoinTree(EntityInflater<C, I> rootEntityInflater, Fromable table) {
this.root = new JoinRoot<>(this, rootEntityInflater, table);
this.joinIndex.put(ROOT_JOIN_NAME, root);
}
public EntityJoinTree(Function<EntityJoinTree<C, I>, JoinRoot<C, I, ?>> joinRootCreator) {
this.root = joinRootCreator.apply(this);
this.joinIndex.put(ROOT_JOIN_NAME, root);
}
public JoinRoot<C, I, ?> getRoot() {
return root;
}
/**
* Returns mapping between {@link JoinNode} and their internal name.
*
* @return an unmodifiable version of the internal mapping (because its maintenance responsibility falls to current class)
*/
@VisibleForTesting
public BidiMap<String, JoinNode> getJoinIndex() {
return UnmodifiableBidiMap.unmodifiableBidiMap(joinIndex);
}
/**
* Declares a {@link Table} to be included in DDL generation.
* To be used for particular use cases because {@link #giveTables()} collects this tree tables.
*
* @param table any {@link Table}
*/
public void addTableToIncludeToDDL(Table<?> table) {
this.tablesToIncludeToDDL.add(table);
}
/**
* Adds a join to this select.
* Use for one-to-one or one-to-many cases when join is used to describe a related bean.
*
* @param <U> type of bean mapped by the given strategy
* @param <T1> joined left table
* @param <T2> joined right table
* @param <ID> type of joined values
* @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
* @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
* @param propertyAccessor accessor to the property of this persister's entity from the source entity type
* @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
* @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
* @param rightTableAlias optional alias for right table, if null table name will be used
* @param joinType says whether the join must be open
* @param beanRelationFixer a function to fulfill relation between beans
* @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
* @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
*/
public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID> String addRelationJoin(String leftStrategyName,
EntityInflater<U, ID> inflater,
Accessor<?, ?> propertyAccessor,
Key<T1, ID> leftJoinColumn,
Key<T2, ID> rightJoinColumn,
@Nullable String rightTableAlias,
JoinType joinType,
BeanRelationFixer<C, U> beanRelationFixer,
Set<? extends Column<T2, ?>> additionalSelectableColumns) {
return addRelationJoin(leftStrategyName, inflater, propertyAccessor, leftJoinColumn, rightJoinColumn, rightTableAlias, joinType, beanRelationFixer, additionalSelectableColumns, null);
}
/**
* Adds a join to this select.
* Use for one-to-one or one-to-many cases when join is used to describe a related bean.
* Difference with {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)} is last
* parameter : an optional function which computes an identifier of a relation between 2 entities, this is required to prevent from fulfilling
* twice the relation when SQL returns several times same identifier (when at least 2 collections are implied). By default this function is
* made of parentEntityId + childEntityId and can be overwritten here (in particular when relation is a List, entity index is added to computation).
* See {@link RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(Object, ColumnedRow, TreeInflationContext)} for usage.
*
* @param <U> type of bean mapped by the given strategy
* @param <T1> joined left table
* @param <T2> joined right table
* @param <ID> type of joined values
* @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
* @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
* @param propertyAccessor accessor to the property of this persister's entity from the source entity type
* @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
* @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
* @param rightTableAlias optional alias for right table, if null table name will be used
* @param joinType says whether the join must be open
* @param beanRelationFixer a function to fulfill relation between beans
* @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
* @param relationIdentifierProvider relation identifier provider, not null for List cases : necessary because List may contain duplicate
* @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
* @see RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(Object, ColumnedRow, TreeInflationContext)
*/
public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID, JOINTYPE> String addRelationJoin(String leftStrategyName,
EntityInflater<U, ID> inflater,
Accessor<?, ?> propertyAccessor,
Key<T1, JOINTYPE> leftJoinColumn,
Key<T2, JOINTYPE> rightJoinColumn,
@Nullable String rightTableAlias,
JoinType joinType,
BeanRelationFixer<?, U> beanRelationFixer,
Set<? extends Column<T2, ?>> additionalSelectableColumns,
@Nullable Function<ColumnedRow, Object> relationIdentifierProvider) {
return this.addJoin(leftStrategyName, parent -> {
Duo<T2, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(rightJoinColumn.getTable());
// Build a new Key using the cloned table and the corresponding cloned columns
Key.KeyBuilder<T2, JOINTYPE> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
Set<? extends JoinLink<?, ?>> columns = rightJoinColumn.getColumns();
for (JoinLink<?, ?> column : columns) {
// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
JoinLink<T2, Object> clonedColumn = (JoinLink<T2, Object>) tableClone.getRight().get(column);
rightJoinLinkBuilder.addColumn(clonedColumn);
}
// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set
IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = tableClone.getRight();
return new RelationJoinNode<U, T1, T2, JOINTYPE, ID>(
(JoinNode) parent,
propertyAccessor,
leftJoinColumn,
rightJoinLinkBuilder.build(),
joinType,
new KeepOrderSet<>(Collections.cat(inflater.getSelectableColumns(), additionalSelectableColumns)),
rightTableAlias,
inflater,
beanRelationFixer,
relationIdentifierProvider,
originalColumnsToClones);
});
}
/**
* Adds a join to this select.
* Use for inheritance cases when joined data are used to complete an existing bean.
*
* @param <U> type of bean mapped by the given strategy
* @param <T1> left table type
* @param <T2> right table type
* @param <ID> type of joined values
* @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}.
* Right table data will be merged with this "root".
* @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
* @param leftJoinColumn the {@link Column} (of previous strategy left table) to be joined with {@code rightJoinColumn}
* @param rightJoinColumn the {@link Column} (of the strategy table) to be joined with {@code leftJoinColumn}
* @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
*/
public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
EntityMerger<U> inflater,
Key<T1, ID> leftJoinColumn,
Key<T2, ID> rightJoinColumn) {
return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
leftJoinColumn, rightJoinColumn, JoinType.INNER,
null, inflater));
}
/**
* Adds a merge join to this select : no bean will be created by given {@link EntityInflater}, only its
* {@link AbstractTransformer#applyRowToBean(ColumnedRow, Object)} will be used during bean graph loading process.
*
* @param <T1> left table type
* @param <T2> right table type
* @param <ID> type of joined values
* @param leftStrategyName join name on which join must be created
* @param entityMerger strategy to be used to load bean
* @param leftJoinColumn left join column, expected to be one of left strategy table
* @param rightJoinColumn right join column
* @param joinType type of join to create
* @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
*/
public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
EntityMerger<U> entityMerger,
Key<T1, ID> leftJoinColumn,
Key<T2, ID> rightJoinColumn,
JoinType joinType) {
return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
leftJoinColumn, rightJoinColumn, joinType,
null, entityMerger));
}
public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
EntityMerger<U> entityMerger,
Key<T1, ID> leftJoinColumn,
Key<T2, ID> rightJoinColumn,
JoinType joinType,
Function<JoinNode<U, T2>, MergeJoinRowConsumer<U>> columnedRowConsumer) {
return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<U, T1, T2, ID>((JoinNode<?, T1>) parent,
leftJoinColumn, rightJoinColumn, joinType,
null, entityMerger) {
@Override
public MergeJoinRowConsumer<U> toConsumer(JoinNode<U, T2> joinNode) {
return columnedRowConsumer.apply(joinNode);
}
});
}
/**
* Adds a passive join to this select : this kind of join doesn't take part to bean construction, it aims only at adding an SQL join to
* bean graph loading.
*
* @param leftStrategyName join name on which join must be created
* @param leftJoinColumn left join column, expected to be one of left strategy table
* @param rightJoinColumn right join column
* @param joinType type of join to create
* @param columnsToSelect columns that must be added to final select
* @param <T1> left table type
* @param <T2> right table type
* @param <JOINTYPE> type of joined values
* @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
*/
public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
Key<T1, JOINTYPE> leftJoinColumn,
Key<T2, JOINTYPE> rightJoinColumn,
JoinType joinType,
Set<? extends JoinLink<T2, ?>> columnsToSelect) {
return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
leftJoinColumn, rightJoinColumn, joinType,
columnsToSelect, null));
}
public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
Key<T1, JOINTYPE> leftJoinColumn,
Key<T2, JOINTYPE> rightJoinColumn,
JoinType joinType,
Set<? extends Selectable<?>> columnsToSelect,
EntityTreeJoinNodeConsumptionListener<C> consumptionListener) {
return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
leftJoinColumn, rightJoinColumn, joinType,
columnsToSelect, null).setConsumptionListener(consumptionListener));
}
public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
Key<T1, JOINTYPE> leftJoinColumn,
Key<T2, JOINTYPE> rightJoinColumn,
String tableAlias,
JoinType joinType,
Set<? extends Selectable<?>> columnsToSelect,
EntityTreeJoinNodeConsumptionListener<C> consumptionListener,
boolean rightTableParticipatesToDDL) {
if (!rightTableParticipatesToDDL) {
tablesToExcludeFromDDL.add(rightJoinColumn.getTable());
}
return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
leftJoinColumn, rightJoinColumn, joinType,
columnsToSelect, tableAlias).setConsumptionListener(consumptionListener));
}
public String addJoin(String leftStrategyName, Function<? super JoinNode<?, Fromable> /* parent node */, ? extends AbstractJoinNode<?, ?, ?, ?>> joinNodeSupplier) {
JoinNode<?, Fromable> owningJoin = getJoin(leftStrategyName);
if (owningJoin == null) {
throw new IllegalArgumentException("No join named " + leftStrategyName + " exists to add a new join on");
}
AbstractJoinNode joinNode = joinNodeSupplier.apply(owningJoin);
String joinName = this.indexKeyGenerator.generateKey(joinNode);
this.joinIndex.put(joinName, joinNode);
return joinName;
}
/**
* Gives a particular node of the joins graph by its name. Joins graph name are given in return of
* {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)}.
* When {@link #ROOT_JOIN_NAME} is given, {@link #getRoot()} will be used, meanwhile, be aware that using this method to retreive root node
* is not the recommended way : prefer usage of {@link #getRoot()} to prevent exposure of {@link #ROOT_JOIN_NAME}
*
* @param leftStrategyName join node name to be given
* @return null if the node doesn't exist
* @see #getRoot()
*/
@Nullable
public JoinNode<?, Fromable> getJoin(String leftStrategyName) {
return (JoinNode<?, Fromable>) this.joinIndex.get(leftStrategyName);
}
/**
* Gives all tables used by this tree
*
* @return all joins tables of this tree
*/
public Set<Table<?>> giveTables() {
// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
Set<Table<?>> result = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
extractTable(getRoot(), result);
foreachJoin(node -> {
// for now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains both join link values and
// columns to select, and the former is the one of the abstract entity that doesn't match a concrete table (because of table-per-class model);
// but sub-tables are addressed because the node contains the joins to the sub-tables.
// For now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains PseudoColumns
// to select which are not part of a concrete Table. Note that the sub-tables are still addressed because the node contains the joins
// to the sub-tables.
if (!(node instanceof TablePerClassPolymorphicRelationJoinNode)) {
extractTable(node, result);
}
});
tablesToIncludeToDDL.forEach(result::add);
return result;
}
private void extractTable(JoinNode<?, ?> node, Set<Table<?>> result) {
// we must take the original table because that's where user add the foreign keys, because the table cloning mechanism doesn't propagate them
node.getOriginalColumnsToLocalOnes().keySet().forEach(column -> {
if (column instanceof Column) {
Fromable table = ((Column) column).getTable();
if (!tablesToExcludeFromDDL.contains(table)) {
result.add((Table<?>) table);
}
}
});
}
/**
* Shortcut for {@code joinIterator().forEachRemaining()}.
* Goes down this tree by breadth first. Made to avoid everyone implements node iteration.
* Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
* Traversal is made in pre-order : node is consumed first then its children.
*
* @param consumer a {@link AbstractJoinNode} consumer
*/
public void foreachJoin(Consumer<AbstractJoinNode<?, ?, ?, ?>> consumer) {
joinIterator().forEachRemaining(consumer);
}
/**
* Copies this tree onto given one under given join node name
*
* @param target tree receiving copies of this tree nodes
* @param joinNodeName node name under which this tree must be copied,
* @param <E> main entity type of target tree
* @param <ID> main entity identifier type of target tree
*/
public <E, ID> void projectTo(EntityJoinTree<E, ID> target, String joinNodeName) {
projectTo(target.getJoin(joinNodeName));
}
public void projectTo(JoinNode<?, Fromable> joinNode) {
EntityJoinTree<?, ?> tree = joinNode.getTree();
foreachJoinWithDepth(joinNode, (targetOwner, currentNode) -> {
// cloning each node, the only difference lays on left column : target gets its matching column
currentNode.getLeftJoinLink().getColumns().forEach(leftColumn -> {
Selectable<?> projectedLeftColumn = targetOwner.getTable().findColumn(leftColumn.getExpression());
if (projectedLeftColumn == null) {
throw new IllegalArgumentException("Expected column "
+ leftColumn.getExpression() + " to exist in target table " + targetOwner.getTable().getName()
+ " but couldn't be found among " + Iterables.collect(targetOwner.getTable().getColumns(), Selectable::getExpression, ArrayList::new));
}
});
AbstractJoinNode nodeClone = cloneNodeForParent(currentNode, targetOwner, currentNode.getLeftJoinLink());
// maintaining join names through trees : we add current node name to target one. Then nodes can be found across trees
Set<Entry<String, JoinNode<?, ?>>> set = this.joinIndex.entrySet();
Entry<String, JoinNode<?, ?>> nodeName = Iterables.find(set, entry -> entry.getValue() == currentNode);
tree.joinIndex.put(nodeName.getKey(), nodeClone);
return nodeClone;
});
}
/**
* Creates an {@link Iterator} that goes down this tree by breadth first. Made to avoid everyone implements node iteration.
* Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
* Traversal is made in pre-order : node is consumed first then its children.
*/
public Iterator<AbstractJoinNode<?, ?, ?, ?>> joinIterator() {
return new NodeIterator();
}
/**
* Goes down this tree by breadth first.
* Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
* Used to create an equivalent tree of this instance with another type of node. This generally requires knowing current parent to allow child
* addition : consumer gets current parent as a first argument
*
* @param initialNode very first parent given as first argument to consumer
* @param consumer producer of target tree node, gets previous created node (to add created node to it) and node of this tree
* @param <S> type of node of the equivalent tree
*/
<S> void foreachJoinWithDepth(S initialNode, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
Queue<S> targetPath = new ArrayDeque<>();
targetPath.add(initialNode);
NodeIteratorWithDepth<S> nodeIterator = new NodeIteratorWithDepth<>(targetPath, consumer);
// We simply iterate all over the iterator to consume all elements
// Please note that forEachRemaining can't be used because it is unsupported by NodeIteratorWithDepth
while (nodeIterator.hasNext()) {
nodeIterator.next();
}
}
/**
* Internal class that focuses on nodes. Iteration node is made breadth-first.
*/
private class NodeIterator implements Iterator<AbstractJoinNode<?, ?, ?, ?>> {
protected final Queue<AbstractJoinNode> joinStack;
protected AbstractJoinNode currentNode;
protected boolean nextDepth = false;
public NodeIterator() {
joinStack = new ArrayDeque<>(root.getJoins());
}
@Override
public boolean hasNext() {
return !joinStack.isEmpty();
}
@Override
@SuppressWarnings("java:S2272") // NoSuchElementException is manged by Queue#remove()
public AbstractJoinNode next() {
// we prefer remove() to poll() because it manages NoSuchElementException which is also in next() contract
currentNode = joinStack.remove();
ReadOnlyList<AbstractJoinNode> nextJoins = currentNode.getJoins();
joinStack.addAll(nextJoins);
nextDepth = !nextJoins.isEmpty();
return currentNode;
}
}
private class NodeIteratorWithDepth<S> extends NodeIterator {
private final Queue<S> targetPath;
private final BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S> consumer;
public NodeIteratorWithDepth(Queue<S> targetPath, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
this.targetPath = targetPath;
this.consumer = (BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S>) (BiFunction) consumer;
}
@Override
public AbstractJoinNode next() {
super.next();
S targetOwner = targetPath.peek();
S nodeClone = (S) consumer.apply(targetOwner, currentNode);
if (nextDepth) {
targetPath.add(nodeClone);
}
// if depth changes, we must remove target depth
AbstractJoinNode nextIterationNode = joinStack.peek();
if (nextIterationNode != null && nextIterationNode.getParent() != currentNode.getParent()) {
targetPath.remove();
}
return currentNode;
}
@Override
public void forEachRemaining(Consumer<? super AbstractJoinNode<?, ?, ?, ?>> action) {
// this is not supported since a consumer is already given to constructor
throw new UnsupportedOperationException();
}
}
/**
* Clones table of given join (only on its columns, no need for its foreign key clones nor indexes)
*
* @param fromable the table to clone
* @return a copy (on name and columns) of given join table
*/
static <T extends Fromable> Duo<T, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> cloneTable(T fromable) {
if (fromable instanceof Table) {
Table<?> table = (Table<?>) fromable;
Table tableClone = new Table(fromable.getName());
IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(tableClone.getColumns().size());
(((Table<?>) fromable).getColumns()).forEach(column -> {
Column<?, ?> clone = tableClone.addColumn(column.getName(), column.getJavaType(), column.getSize(), column.isNullable());
columnClones.put(column, clone);
});
// Propagating primary key because right tables are used to generate schema, and at this stage they lack primary key.
// Note that foreign keys will be added through the tree building process when appending joins, so we don't need to clone them here
if (table.getPrimaryKey() != null) {
Key<?, ?> primaryKey = table.getPrimaryKey();
primaryKey.getColumns().forEach(column -> {
// we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
Column<?, ?> clonedColumn = (Column<?, ?>) columnClones.get(column);
clonedColumn.primaryKey();
});
}
return new Duo<>((T) tableClone, columnClones);
} else if (fromable instanceof PseudoTable) {
PseudoTable pseudoTable = new PseudoTable(((PseudoTable) fromable).getQueryStatement(), fromable.getName());
IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(pseudoTable.getColumns().size());
(((PseudoTable) fromable).getColumns()).forEach(column -> {
// we can only have Union in From clause, no sub-query, because of table-per-class polymorphism, so we can cast to Union
PseudoColumn<?> clone = ((Union) pseudoTable.getQueryStatement()).registerColumn(column.getExpression(), column.getJavaType());
columnClones.put(column, clone);
});
return new Duo<>((T) pseudoTable, columnClones);
} else {
throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
}
}
static <T extends Fromable> Key.KeyBuilder<T, ?> mimicKey(Key<?, ?> leftJoinColumn, T fromable) {
if (fromable instanceof Table) {
Table<?> table = (Table<?>) fromable;
Key.KeyBuilder<Table, ?> leftKeyBuilder = Key.from(table);
leftJoinColumn.getColumns().forEach(column -> {
Column column1 = table.addColumn(column.getExpression(), column.getJavaType());
leftKeyBuilder.addColumn(column1);
});
return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
} else if (fromable instanceof PseudoTable) {
PseudoTable union = (PseudoTable) fromable;
Key.KeyBuilder<PseudoTable, ?> leftKeyBuilder = Key.from(union);
leftJoinColumn.getColumns().forEach(column -> {
Selectable<?> column1 = union.findColumn(column.getExpression());
leftKeyBuilder.addColumn((JoinLink<PseudoTable, ?>) column1);
});
return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
} else {
throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
}
}
/**
* Copies given node and set it as a child of given parent.
* Could have been implemented by each node class itself but since this behavior is required only by the tree
* and a particular algorithm, decision was made to do it outside of them.
*
* @param node node to be cloned
* @param parent parent node target of the clone
* @param leftJoinColumn columns to be used as the left key of the new node
* @return a copy of given node, put as child of parent, using leftColumn
*/
public static AbstractJoinNode<?, ?, ?, ?> cloneNodeForParent(AbstractJoinNode<?, ?, ?, ?> node, JoinNode parent, Key<?, ?> leftJoinColumn) {
Duo<Fromable, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(node.getTable());
// Build a new Key using the cloned table and the corresponding cloned columns
Key.KeyBuilder<Fromable, Object> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
Set<? extends JoinLink<?, ?>> columns = node.getRightJoinLink().getColumns();
for (JoinLink<?, ?> column : columns) {
// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
JoinLink<Fromable, Object> clonedColumn = (JoinLink<Fromable, Object>) tableClone.getRight().get(column);
rightJoinLinkBuilder.addColumn(clonedColumn);
}
// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set
IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = Maps.innerJoinOnValuesAndKeys(node.getOriginalColumnsToLocalOnes(), tableClone.getRight(), IdentityHashMap::new);
Key.KeyBuilder<Fromable, ?> leftJoinLinkBuilder = mimicKey(leftJoinColumn, parent.getTable());
AbstractJoinNode nodeCopy;
if (node instanceof RelationJoinNode) {
nodeCopy = new RelationJoinNode(
parent,
((RelationJoinNode<?, ?, ?, ?, ?>) node).getPropertyAccessor(),
leftJoinLinkBuilder.build(),
rightJoinLinkBuilder.build(),
node.getJoinType(),
node.getColumnsToSelect(),
node.getTableAlias(),
((RelationJoinNode) node).getEntityInflater(),
((RelationJoinNode) node).getBeanRelationFixer(),
((RelationJoinNode) node).getRelationIdentifierProvider(),
originalColumnsToClones);
} else if (node instanceof MergeJoinNode) {
nodeCopy = new MergeJoinNode(
parent,
leftJoinLinkBuilder.build(),
rightJoinLinkBuilder.build(),
node.getJoinType(),
node.getTableAlias(),
((MergeJoinNode) node).getMerger(),
node.getColumnsToSelect(),
originalColumnsToClones);
} else if (node instanceof PassiveJoinNode) {
nodeCopy = new PassiveJoinNode(
parent,
leftJoinLinkBuilder.build(),
rightJoinLinkBuilder.build(),
node.getJoinType(),
node.getColumnsToSelect(),
node.getTableAlias(),
originalColumnsToClones);
} else {
throw new UnsupportedOperationException("Unexpected type of join : some algorithm has changed, please implement it here or fix it : "
+ Reflections.toString(node.getClass()));
}
nodeCopy.setConsumptionListener(node.getConsumptionListener());
return nodeCopy;
}
private class NodeKeyGenerator {
/**
* We don't use {@link java.security.SecureRandom} because it consumes too much time while computing random values (more than 500ms
* for a 6-digit identifier ! ... for each Node !!), and there's no need for security here.
*/
@SuppressWarnings("java:S2245" /* no security issue about Random class : it's only used as identifier generator, using SecureRandom is less performant */)
private final Randomizer keyGenerator = new Randomizer(new LinearRandomGenerator(new Random()));
private String generateKey(JoinNode node) {
// We generate a name which is unique across trees so node clones can be found outside this class by their name on different trees.
// This is necessary for particular case of reading indexing column of indexed collection with an association table : the node that needs
// to use indexing column is not the owner of the association table clone hence it can't use it (see table clone mechanism at EntityTreeQueryBuilder).
// Said differently the "needer" is the official owner whereas the indexing column is on another node dedicated to the relation table maintenance.
// The found way for the official node to access data through the indexing column is to use the identifier of the relation table node,
// because it has it when it is created (see OneToManyWithIndexedAssociationTableEngine), and because the node is cloned through tree
// the identifier should be "universal".
// Note that this naming strategy could be more chaotic (totally random) since names are only here to give a unique identifier to joins
// but the hereafter algorithm can help for debug
return node.getTable().getAbsoluteName()
+ "-" + Integer.toHexString(System.identityHashCode(EntityJoinTree.this)) + "-" + keyGenerator.randomHexString(6);
}
}
public enum JoinType {
INNER,
OUTER
}
}